MCP server

The AI Agents framework exposes a Model Context Protocol (MCP) server so that MCP clients (such as Claude Desktop and Claude Code) can provision and troubleshoot voice AI applications — creating and editing agents, tools, documents and flows, chatting with agents to test them, inspecting past conversations, and reading this documentation.

The server is embedded in the management service and exposed over streamable HTTP at:

/ai-framework-management/api/v1/mcp/

Always connect with the trailing slash (.../api/v1/mcp/). The endpoint is served at the trailing-slash path; a request to the slash-less URL is answered with an HTTP redirect, and some MCP clients — notably the mcp-remote stdio bridge that Claude Desktop uses — don't follow that redirect on a POST and fail to connect.

Connecting

Two MCP clients are supported today: Claude Code and Claude Desktop. Both connect to the same endpoint using your LiveHub API client's client_id and client_secret: the server handles authentication for you and keeps the short-lived token refreshed, so there is no token to manage. The identity determines the account, and all operations are scoped to it. If you don't have these credentials yet, see Getting your client_id and client_secret below.

Getting your client_id and client_secret

Your client_id and client_secret belong to a LiveHub API client, which you create — and grant the Administrator role — from the Access control (IAM) screen:

  1. Click your account name shown at the top of the screen.

  2. In the account dialog, click Access control (IAM).

  3. In the IAM screen, select API Clients in the left menu, then click Add API Client.

    • Enter a name for the API client — for example, claude.
    • Copy the generated Client Id and Client Secret. The Client Secret is shown only once, so save it now — it will not be visible again after you close the screen.
  4. Grant the new API client the Administrator role so it can provision and manage entities — select User groups in the left menu.

    • Find the Administrator group in the list and click Edit.
    • Switch to the API Clients tab, click Add API Client, and select the API client you created above.

Use the Client Id as X-Client-Id and the Client Secret as X-Client-Secret in the connection settings below.

Claude Code

Register the server with claude mcp add:

claude mcp add --transport http livehub-ai-agents https://livehub.audiocodes.io/ai-framework-management/api/v1/mcp/ \
  --header "X-Client-Id: <id>" --header "X-Client-Secret: <secret>"

Claude Desktop

Claude Desktop connects to the streamable-HTTP endpoint through the mcp-remote bridge. Add the following to your claude_desktop_config.json (note the trailing slash on the URL — see the note above), then restart Claude Desktop:

{
  "mcpServers": {
    "livehub-ai-agents": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://livehub.audiocodes.io/ai-framework-management/api/v1/mcp/",
        "--header", "X-Client-Id:<id>",
        "--header", "X-Client-Secret:<secret>"
      ]
    }
  }
}

Clients that support only one custom header

Some MCP clients let you set only a single custom header. For those, pass both credentials in one X-Client-Credentials header, as <client_id>:<client_secret> (client id, a colon, then client secret).

Clients that use HTTP Basic authentication

For clients whose only authentication affordance is standard HTTP Basic auth, pass the client_id as the username and the client_secret as the password. The client encodes them into the standard Authorization: Basic <base64(client_id:client_secret)> header, which the server accepts in place of the X-Client-* headers.

Providing your own access token

As an alternative to passing the X-Client-Id / X-Client-Secret headers, you can obtain a LiveHub access token yourself (see Secured REST API) and supply it as a standard Authorization: Bearer header instead:

claude mcp add --transport http livehub-ai-agents https://livehub.audiocodes.io/ai-framework-management/api/v1/mcp/ \
  --header "Authorization: Bearer <access-token>"

A LiveHub access token is valid for one hour. Because you supply it once, at the start of the MCP session, you must acquire a fresh token for every new session — and re-acquire it partway through a long session, since the connection keeps sending the same token and calls start failing once it expires. This is exactly why X-Client-Id / X-Client-Secret is preferred: the server re-acquires and refreshes the token for you transparently.

Read-only mode

Append ?mode=read-only to the URL to restrict the connection to reading and troubleshooting:

https://livehub.audiocodes.io/ai-framework-management/api/v1/mcp/?mode=read-only

A read-only connection sees only the read, documentation, conversation-troubleshooting and chat tools; the provisioning tools (apply, create_or_update_*, delete_entity) are hidden and any attempt to call them is rejected. Reconnect without the parameter to make changes.

Core concepts

The platform builds voice AI applications as an orchestration layer over LLMs. Its entities are:

Everything is referenced by name, never by internal id. The only exception is conversations, which have no name and are addressed by their conversation_id.

Writing prompts (voice)

Replies are spoken aloud (TTS) and the caller's speech arrives as text (STT), and there is no automatic system prompt — so a voice app behaves well only if its own prompts say so. Bake these guardrails into your top-level prompt — an agent's prompt or a flow's global prompt (phrased for the agent's or flow's role):

A flow concatenates its global prompt with the active node's prompt, so these guardrails stay in force at every node — keep each node prompt short and focused on that node's task rather than restating them. (A say node's text is the exception: it is spoken to the caller verbatim, bypassing the LLM, so write it directly as plain, pronounceable prose.)

The server surfaces the same guidance in its MCP instructions, so a connected coding agent applies it when it writes prompts.

Tools

Read / inspect

Provision / edit

Test / troubleshoot

Documentation

Provisioning with apply

apply takes a manifest that groups entities by type and references them by name. References are resolved against existing entities plus everything in the same manifest, so you can create an agent together with the tools and documents it depends on in a single call — order doesn't matter.

{
  "documents": [
    { "name": "faq", "urls": "https://example.com/faq\n" }
  ],
  "tools": [
    { "name": "get_rate", "type": "rest", "method": "GET",
      "url": "https://api.example.com/rate/{currency}",
      "params": [ { "name": "currency", "type": "str", "required": true } ] }
  ],
  "agents": [
    { "name": "support", "llm": "gpt-4o", "prompt": "You are a support agent...",
      "documents": ["faq"],
      "tools_config": [ { "tool": "custom", "tool_id": "get_rate" } ] }
  ]
}

apply is strict by default: if any reference cannot be resolved, the whole call fails and names the missing entity — nothing is partially created. Pass permissive: true to drop unresolved references instead.

An existing entity with the same name is replaced wholesale, not merged — every field you omit is reset to its default (a partial agent would wipe its prompt, tools, documents, and so on). This is unlike the create_or_update_* tools, which patch (omitted fields keep their current value). Use apply to (re)define complete entities or provision a new dependency graph; to change a few fields on an existing entity, use the matching create_or_update_* tool.

Adding a dependency to an existing agent. To create a new dependency (for example a post-call-analysis insights) and reference it from an existing agent, do not put a partial agent in the manifest — that would wipe the agent. Either create just the dependency with apply and then call create_or_update_agent with only the changed reference (it merges), or include the agent in the manifest with its complete current config (read it back with get_agent first).

To attach an uploaded file to a document, include it inline:

{ "documents": [ { "name": "handbook",
    "files": [ { "name": "handbook.pdf", "content_base64": "<base64>" } ] } ] }

Use text instead of content_base64 for plain-text content.

Documents parse asynchronously

Creating a document returns immediately; parsing (crawling, chunking, embedding) runs in the background and can take minutes. The document's status moves from creating to created (or parsing failed).

Before chatting with an agent that relies on a document, call wait_for_documents(["<name>"]). It is a bounded wait (returns within ~30 seconds) — if the document is still parsing it returns it under pending, so call it again until the status is terminal.

Authoring flows (FlowSpec)

get_flow and create_or_update_flow use a friendly FlowSpec: flow-level fields plus an inline nodes list, where nodes reference each other by name through transitions, next_node, skip_node, else_node, failed_node and the flow's start_node.

Every flow needs an entry point: set the flow-level start_node to the name of the node the conversation begins at. There is no separate "start" node to add — it's implicit (if you do include a flavor: "start" node, its next_node is folded into start_node and the node is dropped).

Node names must be unique within the flow. (When reading a flow authored in the dashboard, any duplicate node names are disambiguated with a #N suffix so the result round-trips.)

{
  "name": "booking",
  "llm": "gpt-4o",
  "start_node": "greet",
  "nodes": [
    { "name": "greet", "data": { "flavor": "conversation", "behavior": "say", "text": "Hello!",
        "transitions": [ { "condition": "wants to book", "node": "collect" } ] } },
    { "name": "collect", "data": { "flavor": "conversation", "behavior": "prompt",
        "text": "Ask for the date and time." } }
  ]
}

A node can call an API in either of two ways: an API node (data.flavor: "api" — an inline REST request with its own url/method/auth/params) or a Tool node (data.flavor: "tool", which references a separate custom tool). Use whichever fits — an inline API node for a one-off call, a Tool node to reuse a named tool across flows and agents.

Nodes that reference other agents or sub-flows (the pass flavor) are not supported by create_or_update_flow; provision those with apply instead.

Test suites

A test suite targets one agent or flow and holds a list of test cases. Each case is an LLM-driven conversation (a content prompt, or fixed phrases) scored against its success_criteria; pass_threshold / fail_threshold turn the score into pass / warning / fail.

{
  "name": "booking-smoke",
  "agent": "support",
  "target_type": "agent",
  "llm": "gpt-4o",
  "tests": [
    { "name": "greets the caller", "content": "Say hello and ask to book a table.",
      "success_criteria": "The agent greets the caller and offers to help with a booking.",
      "max_turns": 6 }
  ]
}

run_test_suite("booking-smoke") starts a run on the runtime and returns a run_id; the run is asynchronous. Call wait_for_test_run(run_id) to get the results — like wait_for_documents, it is a bounded wait (returns within ~30 seconds), so if the run is still going it comes back with the current progress; call it again until test_status is terminal (completed / failed / cancelled). You can also poll get_test_run(run_id) yourself. Each per-test result carries a score and the conversation_id of the test conversation, so you can get_conversation(conversation_id) to see exactly what happened.

Troubleshooting a conversation

  1. search_conversations(agent="support", start_time="2026-07-01T00:00:00") — find recent calls.
  2. get_conversation("<id>") (or "latest") — read the transcript. Log entries carry a category such as tool_call, task_switch or warning, so you can see what the agent did, not just what it said.
  3. Fix the agent (create_or_update_agent / apply), then start_chat + send_chat_message to verify.

Notes